using System;
using System.Collections;
using System.Globalization;
using System.Text;

/* Taken from http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
 * MIT Licensed: http://www.opensource.org/licenses/mit-license.php
 */

namespace Net.FabreJean.DataMaker
{
	/// <summary>
	/// This class encodes and decodes JSON strings.
	/// Spec. details, see http://www.json.org/
	/// 
	/// JSON uses Arrays and Objects. These correspond here to the datatypes ArrayList and Hashtable.
	/// All numbers are parsed to floats or ints.
	/// </summary>
	public class JSON
	{
		public const int TOKEN_NONE = 0;
		public const int TOKEN_CURLY_OPEN = 1;
		public const int TOKEN_CURLY_CLOSE = 2;
		public const int TOKEN_SQUARED_OPEN = 3;
		public const int TOKEN_SQUARED_CLOSE = 4;
		public const int TOKEN_COLON = 5;
		public const int TOKEN_COMMA = 6;
		public const int TOKEN_STRING = 7;
		public const int TOKEN_NUMBER = 8;
		public const int TOKEN_TRUE = 9;
		public const int TOKEN_FALSE = 10;
		public const int TOKEN_NULL = 11;

		private const int BUILDER_CAPACITY = 2000;
		/// <summary>
		/// Parses the string json into a value
		/// </summary>
		/// <param name="json">A JSON byte array.</param>
		/// <returns>An ArrayList, a Hashtable, a double, a string, null, true, or false</returns>
		public static object JsonDecode (byte[] json) {
			return JsonDecode(System.Text.ASCIIEncoding.ASCII.GetString(json));
		}
		
		/// <summary>
		/// Parses the string json into a value
		/// </summary>
		/// <param name="json">A JSON string.</param>
		/// <returns>An ArrayList, a Hashtable, a double, a string, null, true, or false</returns>
		public static object JsonDecode (string json)
		{
			bool success = true;
			
			return JsonDecode (json, ref success);
		}

		/// <summary>
		/// Parses the string json into a value; and fills 'success' with the successfullness of the parse.
		/// </summary>
		/// <param name="json">A JSON string.</param>
		/// <param name="success">Successful parse?</param>
		/// <returns>An ArrayList, a Hashtable, a double, a string, null, true, or false</returns>
		public static object JsonDecode (string json, ref bool success)
		{
			success = true;
			if (json != null) {
				char[] charArray = json.ToCharArray ();
				int index = 0;
				object value = ParseValue (charArray, ref index, ref success);
				return value;
			} else {
				return null;
			}
		}

		/// <summary>
		/// Converts a Hashtable / ArrayList object into a JSON string
		/// </summary>
		/// <param name="json">A Hashtable / ArrayList</param>
		/// <returns>A JSON encoded string, or null if object 'json' is not serializable</returns>
		public static string JsonEncode (object json)
		{
			StringBuilder builder = new StringBuilder (BUILDER_CAPACITY);
			bool success = SerializeValue (json, builder);
			return (success ? builder.ToString () : null);
		}

		protected static Hashtable ParseObject (char[] json, ref int index, ref bool success)
		{
			Hashtable table = new Hashtable ();
			int token;
			
			// {
			NextToken (json, ref index);
			
			bool done = false;
			while (!done) {
				token = LookAhead (json, index);
				if (token == JSON.TOKEN_NONE) {
					success = false;
					return null;
				} else if (token == JSON.TOKEN_COMMA) {
					NextToken (json, ref index);
				} else if (token == JSON.TOKEN_CURLY_CLOSE) {
					NextToken (json, ref index);
					return table;
				} else {
					
					// name
					string name = ParseString (json, ref index, ref success);
					if (!success) {
						success = false;
						return null;
					}
					
					// :
					token = NextToken (json, ref index);
					if (token != JSON.TOKEN_COLON) {
						success = false;
						return null;
					}
					
					// value
					object value = ParseValue (json, ref index, ref success);
					if (!success) {
						success = false;
						return null;
					}
					
					table[name] = value;
				}
			}
			
			return table;
		}

		protected static ArrayList ParseArray (char[] json, ref int index, ref bool success)
		{
			ArrayList array = new ArrayList ();
			
			// [
			NextToken (json, ref index);
			
			bool done = false;
			while (!done) {
				int token = LookAhead (json, index);
				if (token == JSON.TOKEN_NONE) {
					success = false;
					return null;
				} else if (token == JSON.TOKEN_COMMA) {
					NextToken (json, ref index);
				} else if (token == JSON.TOKEN_SQUARED_CLOSE) {
					NextToken (json, ref index);
					break;
				} else {
					object value = ParseValue (json, ref index, ref success);
					if (!success) {
						return null;
					}
					
					array.Add (value);
				}
			}
			
			return array;
		}

		protected static object ParseValue (char[] json, ref int index, ref bool success)
		{
			switch (LookAhead (json, index)) {
			case JSON.TOKEN_STRING:
				return ParseString (json, ref index, ref success);
			case JSON.TOKEN_NUMBER:
				return ParseNumber (json, ref index, ref success);
			case JSON.TOKEN_CURLY_OPEN:
				return ParseObject (json, ref index, ref success);
			case JSON.TOKEN_SQUARED_OPEN:
				return ParseArray (json, ref index, ref success);
			case JSON.TOKEN_TRUE:
				NextToken (json, ref index);
				return true;
			case JSON.TOKEN_FALSE:
				NextToken (json, ref index);
				return false;
			case JSON.TOKEN_NULL:
				NextToken (json, ref index);
				return null;
			case JSON.TOKEN_NONE:
				break;
			}
			
			success = false;
			return null;
		}

		protected static string ParseString (char[] json, ref int index, ref bool success)
		{
			StringBuilder s = new StringBuilder (BUILDER_CAPACITY);
			char c;
			
			EatWhitespace (json, ref index);
			
			// "
			c = json[index++];
			
			bool complete = false;
			while (!complete) {
				
				if (index == json.Length) {
					break;
				}
				
				c = json[index++];
				if (c == '"') {
					complete = true;
					break;
				} else if (c == '\\') {
					
					if (index == json.Length) {
						break;
					}
					c = json[index++];
					if (c == '"') {
						s.Append ('"');
					} else if (c == '\\') {
						s.Append ('\\');
					} else if (c == '/') {
						s.Append ('/');
					} else if (c == 'b') {
						s.Append ('\b');
					} else if (c == 'f') {
						s.Append ('\f');
					} else if (c == 'n') {
						s.Append ('\n');
					} else if (c == 'r') {
						s.Append ('\r');
					} else if (c == 't') {
						s.Append ('\t');
					} else if (c == 'u') {
						int remainingLength = json.Length - index;
						if (remainingLength >= 4) {
							// parse the 32 bit hex into an integer codepoint
							uint codePoint;
							if (!(success = UInt32.TryParse (new string (json, index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint))) {
								return "";
							}
							// convert the integer codepoint to a unicode char and add to string
							s.Append (Char.ConvertFromUtf32 ((int)codePoint));
							// skip 4 chars
							index += 4;
						} else {
							break;
						}
					}
					
				} else {
					s.Append (c);
				}
				
			}
			
			if (!complete) {
				success = false;
				return null;
			}
			
			return s.ToString ();
		}

		protected static object ParseNumber (char[] json, ref int index, ref bool success)
		{
			EatWhitespace (json, ref index);
			
			int lastIndex = GetLastIndexOfNumber (json, index);
			int charLength = (lastIndex - index) + 1;
			
			float number;
			var token = new string (json, index, charLength);
			success = float.TryParse (token, NumberStyles.Any, CultureInfo.InvariantCulture, out number);
			index = lastIndex + 1;
			if(token.Contains("."))
				return (float)number;
			else
				return (int)number;
		}

		protected static int GetLastIndexOfNumber (char[] json, int index)
		{
			int lastIndex;
			
			for (lastIndex = index; lastIndex < json.Length; lastIndex++) {
				if ("0123456789+-.eE".IndexOf (json[lastIndex]) == -1) {
					break;
				}
			}
			return lastIndex - 1;
		}

		protected static void EatWhitespace (char[] json, ref int index)
		{
			for (; index < json.Length; index++) {
				if (" \t\n\r".IndexOf (json[index]) == -1) {
					break;
				}
			}
		}

		protected static int LookAhead (char[] json, int index)
		{
			int saveIndex = index;
			return NextToken (json, ref saveIndex);
		}

		protected static int NextToken (char[] json, ref int index)
		{
			EatWhitespace (json, ref index);
			
			if (index == json.Length) {
				return JSON.TOKEN_NONE;
			}
			
			char c = json[index];
			index++;
			switch (c) {
			case '{':
				return JSON.TOKEN_CURLY_OPEN;
			case '}':
				return JSON.TOKEN_CURLY_CLOSE;
			case '[':
				return JSON.TOKEN_SQUARED_OPEN;
			case ']':
				return JSON.TOKEN_SQUARED_CLOSE;
			case ',':
				return JSON.TOKEN_COMMA;
			case '"':
				return JSON.TOKEN_STRING;
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
			case '-':
				return JSON.TOKEN_NUMBER;
			case ':':
				return JSON.TOKEN_COLON;
			}
			index--;
			
			int remainingLength = json.Length - index;
			
			// false
			if (remainingLength >= 5) {
				if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && json[index + 4] == 'e') {
					index += 5;
					return JSON.TOKEN_FALSE;
				}
			}
			
			// true
			if (remainingLength >= 4) {
				if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') {
					index += 4;
					return JSON.TOKEN_TRUE;
				}
			}
			
			// null
			if (remainingLength >= 4) {
				if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') {
					index += 4;
					return JSON.TOKEN_NULL;
				}
			}
			
			return JSON.TOKEN_NONE;
		}

		protected static bool SerializeValue (object value, StringBuilder builder)
		{
			bool success = true;
			
			if (value is string) {
				success = SerializeString ((string)value, builder);
			} else if (value is Hashtable) {
				success = SerializeObject ((Hashtable)value, builder);
			} else if (value is ArrayList) {
				success = SerializeArray ((ArrayList)value, builder);
			} else if (IsNumeric (value)) {
				success = SerializeNumber (Convert.ToDouble (value), builder);
			} else if ((value is Boolean) && ((Boolean)value == true)) {
				builder.Append ("true");
			} else if ((value is Boolean) && ((Boolean)value == false)) {
				builder.Append ("false");
			} else if (value == null) {
				builder.Append ("null");
			} else {
				success = false;
			}
			return success;
		}

		protected static bool SerializeObject (Hashtable anObject, StringBuilder builder)
		{
			builder.Append ("{");
			
			IDictionaryEnumerator e = anObject.GetEnumerator ();
			bool first = true;
			while (e.MoveNext ()) {
				string key = e.Key.ToString ();
				object value = e.Value;
				
				if (!first) {
					builder.Append (", ");
				}
				
				SerializeString (key, builder);
				builder.Append (":");
				if (!SerializeValue (value, builder)) {
					return false;
				}
				
				first = false;
			}
			
			builder.Append ("}");
			return true;
		}

		protected static bool SerializeArray (ArrayList anArray, StringBuilder builder)
		{
			builder.Append ("[");
			
			bool first = true;
			for (int i = 0; i < anArray.Count; i++) {
				object value = anArray[i];
				
				if (!first) {
					builder.Append (", ");
				}
				
				if (!SerializeValue (value, builder)) {
					return false;
				}
				
				first = false;
			}
			
			builder.Append ("]");
			return true;
		}

		protected static bool SerializeString (string aString, StringBuilder builder)
		{
			builder.Append ("\"");
			
			char[] charArray = aString.ToCharArray ();
			for (int i = 0; i < charArray.Length; i++) {
				char c = charArray[i];
				if (c == '"') {
					builder.Append ("\\\"");
				} else if (c == '\\') {
					builder.Append ("\\\\");
				} else if (c == '\b') {
					builder.Append ("\\b");
				} else if (c == '\f') {
					builder.Append ("\\f");
				} else if (c == '\n') {
					builder.Append ("\\n");
				} else if (c == '\r') {
					builder.Append ("\\r");
				} else if (c == '\t') {
					builder.Append ("\\t");
				} else {
					int codepoint = Convert.ToInt32 (c);
					if ((codepoint >= 32) && (codepoint <= 126)) {
						builder.Append (c);
					} else {
						builder.Append ("\\u" + Convert.ToString (codepoint, 16).PadLeft (4, '0'));
					}
				}
			}
			
			builder.Append ("\"");
			return true;
		}

		protected static bool SerializeNumber (double number, StringBuilder builder)
		{
			builder.Append (Convert.ToString (number, CultureInfo.InvariantCulture));
			return true;
		}

		/// <summary>
		/// Determines if a given object is numeric in any way
		/// (can be integer, double, null, etc). 
		/// 
		/// Thanks to mtighe for pointing out Double.TryParse to me.
		/// </summary>
		protected static bool IsNumeric (object o)
		{
			double result;
			
			return (o == null) ? false : Double.TryParse (o.ToString (), out result);
		}
}			
}
